#include "pch.hpp"
#include "Debugger.hpp"

#include <engine.debugger/Commands.hpp>

using namespace Engine::Edit;




Debugger::Debugger(QFileInfo process, quint16 port, logger::memorystream::ptr logger)
	: m_state(State::Attaching)
	, m_port(port)
	, m_logger(logger)
	, m_dummy(boost::make_shared<char>())
{
	if(!process.exists())
	{
		throw exception("The process cannot be started because it does not exist");
	}

	verify(connect(&m_process, SIGNAL(started()), SLOT(started())));
	verify(connect(&m_process, SIGNAL(error(QProcess::ProcessError)), SLOT(error(QProcess::ProcessError))));
	verify(connect(&m_process, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(processFinished(int, QProcess::ExitStatus))));

	m_processName = process.absoluteFilePath();
	changeState(State::Starting);
	m_process.start(m_processName);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Debugger::~Debugger()
{}
///////////////////////////////////////////////////////////////////////////////////////////////////




Debugger::State::Type Debugger::state() const
{
	return m_state;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

bool Debugger::isAttached() const
{
	return m_connection != nullptr && m_connection->isConnected();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

services::future<void> Debugger::continueProcess()
{
	verify(state() == State::Paused);

	Engine::Debugger::ContinueCommand cmd;
	auto future = m_connection->execute(cmd);

	changeState(State::WaitingForResponse);

	// We want to change the state of the debugger when a response arrives
	// OR when the command times out
	future.ready().connect(services::future<void>::notify::slot_type(
		[&]()
		{
			try
			{
				// If this function throws, then the command was not processed successfully
				future();
				changeState(Debugger::State::Running);
			}
			catch(std::exception&)
			{
				// When something goes wrong, we simply stop the debugger
				stop();
			}
		}
	).track(m_dummy));

	return future;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

services::future<void> Debugger::pause()
{
	verify(state() == State::Running);

	Engine::Debugger::PauseCommand cmd;
	auto future = m_connection->execute(cmd);

	changeState(State::WaitingForResponse);

	future.ready().connect(services::future<void>::notify::slot_type(
		[&]()
		{
			try
			{
				// If this function throws, then the command was not processed successfully
				future();
				changeState(Debugger::State::Paused);
			}
			catch(std::exception&)
			{
				// When something goes wrong, we simply stop the debugger
				stop();
			}
		}
	).track(m_dummy));

	return future;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::detach()
{
	throw boostext::not_implemented_exception();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::stop()
{
	changeState(State::Idle);

	m_logger->log_info(tr("Killing process '%1'").arg(m_processName));

	m_connection->disconnect();
	m_process.kill();
}
///////////////////////////////////////////////////////////////////////////////////////////////////




void Debugger::changeState(State::Type state)
{
	m_state = state;

	switch(m_state)
	{
	case State::Starting:
		m_logger->log_info(tr("Starting process '%1'").arg(m_processName));
		break;
	case State::Attaching:
		m_logger->log_info(tr("Process has started successfully"));
		m_logger->log_info(tr("Attaching debugger using tcp on port %1").arg(m_port));
		break;
	case State::Running:
		m_logger->log_info(tr("Debugger is attached to process, debugging has started"));
		break;
	}

	emit stateChanged();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::started()
{
	m_connection = boost::make_shared<DebuggerConnection>("localhost", m_port);
	verify(connect(m_connection.get(), SIGNAL(disconnected()), SLOT(disconnected())));
	verify(connect(m_connection.get(), SIGNAL(connection_error(QAbstractSocket::SocketError)), SLOT(connection_error(QAbstractSocket::SocketError))));

	changeState(State::Attaching);
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::disconnected()
{
	m_logger->log_info(tr("Debugger disconnecting"));
	stop();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::connection_error(QAbstractSocket::SocketError error)
{
	m_logger->log_error(tr("Debugger connection error: %1").arg(toString(error)));
	stop();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::error(QProcess::ProcessError error)
{
	// We will suppress those messages when the debugger is idle:
	// This means that we've most probably called kill on the process and thus the error
	// is useless
	if(state() != State::Idle)
		m_logger->log_error(toString(error));
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Debugger::processFinished(int exitCode, QProcess::ExitStatus status)
{
	m_logger->log_info(tr("The Process has exited with code: %1").arg(exitCode));

	changeState(State::Idle);
}
///////////////////////////////////////////////////////////////////////////////////////////////////




QString Debugger::toString(QProcess::ProcessError error) const
{
	switch(error)
	{
	case QProcess::FailedToStart: return tr("Unable to start Process");
	case QProcess::Crashed:       return tr("The Process crashed");
	case QProcess::Timedout:      return tr("The Process timed out");
	case QProcess::ReadError:     return tr("Unable to read from the Process");
	case QProcess::WriteError:    return tr("Unable to write to the Process");
	case QProcess::UnknownError:
	default:                      return tr("Unknown");
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////

QString Debugger::toString(QAbstractSocket::SocketError error) const
{
	switch(error)
	{
	case QAbstractSocket::ConnectionRefusedError:           return tr("The connection was refused by the peer (or timed out)");
	case QAbstractSocket::RemoteHostClosedError:            return tr("The remote host closed the connection. Note that the client socket (i.e., this socket) will be closed after the remote close notification has been sent.");
	case QAbstractSocket::HostNotFoundError:                return tr("The host address was not found.");
	case QAbstractSocket::SocketAccessError:                return tr("The socket operation failed because the application lacked the required privileges.");
	case QAbstractSocket::SocketResourceError:              return tr("The local system ran out of resources (e.g., too many sockets).");
	case QAbstractSocket::SocketTimeoutError:               return tr("The socket operation timed out.");
	case QAbstractSocket::DatagramTooLargeError:            return tr("The datagram was larger than the operating system's limit (which can be as low as 8192 bytes).");
	case QAbstractSocket::NetworkError:                     return tr("An error occurred with the network (e.g., the network cable was accidentally plugged out).");
	case QAbstractSocket::AddressInUseError:                return tr("The address specified to QUdpSocket::bind() is already in use and was set to be exclusive.");
	case QAbstractSocket::SocketAddressNotAvailableError:   return tr("The address specified to QUdpSocket::bind() does not belong to the host.");
	case QAbstractSocket::UnsupportedSocketOperationError:  return tr("The requested socket operation is not supported by the local operating system (e.g., lack of IPv6 support).");
	case QAbstractSocket::ProxyAuthenticationRequiredError: return tr("The socket is using a proxy, and the proxy requires authentication.");
	case QAbstractSocket::SslHandshakeFailedError:          return tr("The SSL/TLS handshake failed, so the connection was closed (only used in QSslSocket)");
	case QAbstractSocket::UnfinishedSocketOperationError:   return tr("Used by QAbstractSocketEngine only, The last operation attempted has not finished yet (still in progress in the background).");
	case QAbstractSocket::ProxyConnectionRefusedError:      return tr("Could not contact the proxy server because the connection to that server was denied");
	case QAbstractSocket::ProxyConnectionClosedError:       return tr("The connection to the proxy server was closed unexpectedly (before the connection to the final peer was established)");
	case QAbstractSocket::ProxyConnectionTimeoutError:      return tr("The connection to the proxy server timed out or the proxy server stopped responding in the authentication phase.");
	case QAbstractSocket::ProxyNotFoundError:               return tr("The proxy address set with setProxy() (or the application proxy) was not found.");
	case QAbstractSocket::ProxyProtocolError:               return tr("The connection negotiation with the proxy server because the response from the proxy server could not be understood.");
	case QAbstractSocket::UnknownSocketError:
	default:                                                return tr("An unidentified error occurred.");
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
